S09-04 Webpack5-源码、自定义Loader、自定义Plugin
[TOC]
源码阅读
测试代码
1、在webpack
源码中,编写测试文件夹why
2、直接在build.js
中,调用webpack()
函数,实现打包
3、使用node运行build.js
调试代码
1、添加断点
2、运行调试,点击Javascript调试终端
或 运行和调试
3、调试控制
编译入口文件
思维导图
插件
- Bookmarks
- CodeTour
自定义Loader
Loader API
自定义loader
xxx-loader():
(content, map, meta)
,自定义的loadercontent:``,资源文件的内容
map:``,sourcemap 相关的数据
meta:``,一些元数据
返回:
return:``,
this.callback():
(err, content)
,回调函数- err:
Error | null
,错误信息。如果没错,则传入null - content:
string | buffer
,传递给下个loader的内容
- err:
- js
module.exports = function (content, map, meta) { console.log('xxx-loader: ', content) // console.log('xxx-loader: ', map) // console.log('xxx-loader: ', meta) return 'xxx-loader' }
xxx-loader-pitch():
(remainingRequest, precedingRequest, data)
,一个函数,在加载过程中,Webpack 会调用它来处理文件。remainingRequest:
string
,剩下的请求precedingRequest:
string
,之前处理过的请求data:
object
,loader 共享的数据返回:
不返回:``,继续执行后续的loader
返回值:``,终止执行后续的loader
- js
module.exports.pitch = function pitchLoader(remainingRequest, precedingRequest, data) { // `remainingRequest` 是从当前 loader 到最后一个 loader 的请求路径 // `precedingRequest` 是当前 loader 之前的所有 loader 的请求路径 + // `data` 是 loader 之间共享的数据 // 可以根据 remainingRequest 来决定是否继续处理 if (remainingRequest.includes('specificFile.js')) { // 如果特定文件存在,则忽略后续的 loader return `module.exports = 'This module is handled by pitch loader only.';`; } // 继续使用后续 loader // 返回 null 或者不返回任何值 };
loaderContext
每个 loader 都有一个 loaderContext
对象,loader函数内部的this
指向该对象
this.async():
()
,允许 loader 异步执行。调用async()
方法可以使 loader 进入异步模式,并返回一个异步回调函数,用于处理异步操作。返回:
callback:
(err, content)
,返回一个异步回调函数,用于处理异步操作。err:
Error | null
,错误信息。如果没错,则传入nullcontent:
string | buffer
,传递给下个loader的内容
- js
module.exports = function(source) { const callback = this.async(); setTimeout(() => { callback(null, 'some processed code'); }, 1000); }
this.getOptions():
()
,用于获取传递给 loader 的选项。返回:
options:
object
,在 Webpack 配置中为 loader 指定的选项。- js
// webpack.config.js module.exports = { module: { rules: [ { test: /\.js$/, use: { loader: path.resolve(__dirname, 'my-loader.js'), options: { key1: 'value1', key2: 'value2' } } } ] } }; // loader函数 module.exports = function(source) { const options = this.getOptions(); console.log(options.key1); // 输出: 'value1' console.log(options.key2); // 输出: 'value2' // 处理源代码 return source; };
this.data:
,用来在不同 loader 之间共享数据:状态或配置信息。
- js
// first-loader.js module.exports = function(source) { // 设置数据 this.data = { myData: 'Hello from first-loader' }; return source; }; // second-loader.js module.exports = function(source) { // 读取数据 const data = this.data.myData; console.log(data); // 输出: 'Hello from first-loader' // 继续处理源代码 return source; };
schema-utils
schema-utils:是一个用于验证和处理配置选项的库,通常与 Webpack 的 loader 和插件一起使用。它通过 JSON Schema 定义和验证选项,以确保它们符合预期的格式和类型。
validate():
(schema, options, context?)
,验证选项对象是否符合指定的 JSON Schema。schema:
object
,定义选项格式的 JSON Schema 对象。options:
object
,需要验证的选项对象。context?:
object
,包含额外信息的上下文对象,例如name
和baseDataPath
。返回:
成功:不返回任何值。
失败:抛出错误。
- js
const { validate } = require('schema-utils'); const schema = { type: 'object', properties: { option1: { type: 'string' }, option2: { type: 'number' } }, required: ['option1'], additionalProperties: false }; const options = { option1: 'value', option2: 42 }; try { validate(schema, options, { name: 'my-loader' }); console.log('Options are valid!'); } catch (error) { console.error('Invalid options:', error); }
babel
babel: 是一个广泛使用的 JavaScript 编译器,它提供了一些核心 API 用于代码转换。
注意: 以下API属于 @babel/core 。每个API都有3种模式,如:
transform()
: callbak模式,babel@8中将被删除。transformSync()
: 同步模式transformAsync()
: 异步Promise模式
API:
babel.transformSync():
(code, options)
,用于将源代码转换为不同版本的JS代码。code:
string
,需要转换的 JS 源代码options:
{presets, plugins}
,用于配置 Babel 转换过程的选项。默认已经配置了presets
返回值
result:
{code, map, ast}
,包含转换结果的对象,主要包括code
(转换后的代码)和map
(源映射)。- code:
string
,转换后的JS代码。 - map:
object
,生成的源映射(source map),如果启用了源映射的话。 - ast:
object
,转换后的抽象语法树(AST),如果请求了ast
选项的话。
- code:
- js
const babel = require('@babel/core'); const result = babel.transform('const a = 1;', { presets: ['@babel/preset-env'] }); console.log(result.code); // 编译后的代码
babel.parseSync():
(code, options)
,用于解析源代码并生成抽象语法树(AST)。code:
string
,要解析的源代码。options:
{sourceType, plugins}
,解析的配置选项。返回:
ast:
object
,生成的 AST 对象。- js
const parser = require('@babel/parser'); const ast = parser.parse('const a = 1;', { sourceType: 'module' }); console.log(ast);
transformFromAstSync():
(ast, code?, options)
,用于从 AST 进行转换为代码。ast:
object
,要转换的抽象语法树。code?:
string
,原始源代码(用于错误定位)。options:
object
,Babel 编译选项。返回: Promise
result:
{code, map}
,返回一个对象,包含code
(编译后的代码)、map
(源映射)等信息。- js
const babel = require('@babel/core'); const parser = require('@babel/parser'); const generate = require('@babel/generator').default; const ast = parser.parse('const a = 1;'); const result = babel.transformFromAstSync(ast, 'const a = 1;', { presets: ['@babel/preset-env'] }); console.log(result.code); // 编译后的代码
loadPartialConfig():
(config?)
,用于加载或更新 Babel 的部分配置文件。config?:
object
,包含了你想要更新或加载的部分配置选项。返回:
partialConfig:
PartialConfig
,包含配置的对象,其中包括options
和file
。- js
const babel = require('@babel/core'); // 加载部分配置 const partialConfig = babel.loadPartialConfig({ presets: ['@babel/preset-env'], plugins: ['@babel/plugin-transform-arrow-functions'] }); // 访问配置和其他信息 console.log(partialConfig.config);
marked
marked: 是一个流行的 Markdown 解析器,将 Markdown 转换为 HTML。
API:
new Marked():
(...markedExtension[])
,用于创建Marked
实例的构造函数。markedExtension:
MarkedExtension
,扩展的插件接口。主要用于在marked
的解析过程中插入自定义逻辑。返回:
marked:
object
,返回一个Marked实例。- js
// 使用自定义渲染器 const renderer = new marked.Renderer(); renderer.heading = (text, level) => `<h${level} class="custom-heading">${text}</h${level}>`; const markdown = '# Custom Heading'; const html = marked(markdown, { renderer }); console.log(html); // <h1 class="custom-heading">Custom Heading</h1>
marked.parse():
(markdown, options?)
,用于将 Markdown 文本转换为 HTML。markdown:
string
,要解析的 Markdown 字符串。options:
object
,配置选项对象,用于调整解析和渲染行为。- renderer:
boolean
,自定义渲染器对象,用于修改 HTML 输出。 - gfm:
boolean
,默认:true
,是否启用 GitHub 风格的 Markdown 语法。 - breaks:
boolean
,默认:false
,是否将换行符转换为<br>
标签。 - pedantic:
boolean
,默认:false
,是否宽容解析 Markdown 语法。 - sanitize:
boolean
,默认:false
,是否移除 HTML 标签。 - smartLists:
boolean
,默认:false
,是否优化列表输出。 - smartypants:
boolean
,默认:false
,是否使用智能引号。
- renderer:
返回:
html:
string
,返回HTML字符串。- js
const marked = require('marked'); const markdown = '# Hello, World!\n\nThis is a paragraph with **bold** text.'; const html = marked.parse(markdown); console.log(html); // Output: // <h1>Hello, World!</h1> // <p>This is a paragraph with <strong>bold</strong> text.</p>
marked-highlight
marked-highlight:用于在Markdown中高亮代码的库。它将 marked 和 highlight.js结合。
API:
markedHighlight():
({highlight})
,高亮代码块。highlight:
(code, lang) => void
,转换代码为htmllangPrefix?:
string
,默认:
,class前缀async?:
boolean
,默认:false
,如果highlight方法返回一个Promise,就设置该选项为true返回:
markedExtension :
MarkedExtension
,返回一个扩展的插件接口。主要用于在marked
的解析过程中插入自定义逻辑。- js
const marked = new Marked( markedHighlight({ langPrefix: 'hljs language-', highlight: function (code, lang, info) { const language = hljs.getLanguage(lang) ? lang : 'plaintext' return hljs.highlight(code, { language }).value } }) ) const html = marked.parse(content)
highlight.js
highlight.js:用于高亮代码的库,支持多种编程语言。它的 API 允许你自定义代码高亮的行为。
API:
highlight():
(code, {language?})
,用于高亮代码,支持指定语言。code:
string
,要高亮的代码字符串。language?:
string
,要使用的编程语言。如果未指定,highlight.js
会尝试自动检测语言。返回:
result:
{value}
,返回一个对象,包含value
属性,其中存储了高亮后的 HTML 字符串。- js
const hljs = require('highlight.js'); const code = 'const x = 42;'; const result = hljs.highlight(code, { language: 'javascript' }).value; console.log(result); // Output: <span class="hljs-keyword">const</span> x = <span class="hljs-number">42</span>;
getLanguage():
(lang)
,用于获取特定语言的定义。lang:
string
,语言名称字符串,如'javascript'
、'python'
等。返回:
language:
object
,返回一个包含语言定义的对象,或者如果语言未定义,则返回undefined
。- js
const hljs = require('highlight.js'); // 用于检查 highlight.js 是否支持某种语言 const languageExists = hljs.getLanguage('javascript') !== undefined; console.log(languageExists); // Output: true
自定义Loader
Loader 是用于对模块的源代码进行转换(处理),之前我们已经使用过很多 Loader,比如 css-loader、style-loader、babel-loader 等。
这里我们来学习如何自定义自己的 Loader:
- Loader 本质上是一个导出为函数的 JavaScript 模块;
- 注意: loader导出时,建议使用commonjs语法导出:
module.exports
或exports
- loader-runner 库会调用这个函数,然后将上一个 loader 产生的结果或者资源文件传入进去;
- 注意: loader最终返回的结果必须是模块化的内容
自定义loader
编写一个 xxx-loader01.js 模块这个函数会接收三个参数:
xxx-loader():
(content, map, meta)
,自定义的loadercontent:``,资源文件的内容
map:``,sourcemap 相关的数据
meta:``,一些元数据
返回:
return:``,
this.callback():
(err, content)
,回调函数- err:
Error | null
,错误信息。如果没错,则传入null - content:
string | buffer
,传递给下个loader的内容
- err:
- js
module.exports = function (content, map, meta) { console.log('xxx-loader: ', content) // console.log('xxx-loader: ', map) // console.log('xxx-loader: ', meta) return 'xxx-loader' }
1、自定义一个loader
2、使用loader
3、使用多个loader
4、打包效果
resolveLoader
- resolveLoader:这组选项与 resolve 对象的属性集合相同, 但仅用于解析 webpack 的 loader 包。
- modules:
string[]
,告诉 webpack 解析模块时应该搜索的目录,默认值:['node_modules']
- modules:
如果我们依然希望可以直接去加载自己的 loader 文件夹,有没有更加简洁的办法呢?
1、配置 resolveLoader 属性
注意: 传入的路径和 context 是有关系的,在前面我们讲入口的相对路径时有讲过。context会影响到entry
和loader
中的路径起始点
2、此时loader就可以这样写了,也可以找到
loader执行顺序
loader执行顺序
创建多个 Loader 使用,它的执行顺序是什么呢?
- 从后向前、从右向左
pitch-loader
事实上还有另一种 Loader,称之为 PitchLoader:
pitch loader: 允许你在真正的 loader 之前插入逻辑,并可以决定是否继续处理后续的 loader。
语法:
pitch loader 是一个函数,在加载过程中,Webpack 会调用它来处理文件。
xxx-loader-pitch():
(remainingRequest, precedingRequest, data)
,一个函数,在加载过程中,Webpack 会调用它来处理文件。remainingRequest:
string
,剩下的请求precedingRequest:
string
,之前处理过的请求data:
object
,loader 共享的数据
返回:
不返回:``,继续执行后续的loader
返回值:``,终止执行后续的loader
- js
module.exports.pitch = function pitchLoader(remainingRequest, precedingRequest, data) { // `remainingRequest` 是从当前 loader 到最后一个 loader 的请求路径 // `precedingRequest` 是当前 loader 之前的所有 loader 的请求路径 + // `data` 是 loader 之间共享的数据 // 可以根据 remainingRequest 来决定是否继续处理 if (remainingRequest.includes('specificFile.js')) { // 如果特定文件存在,则忽略后续的 loader return `module.exports = 'This module is handled by pitch loader only.';`; } // 继续使用后续 loader // 返回 null 或者不返回任何值 };
enforce控制执行顺序
loader执行顺序的内部实现:
其实这也是为什么 loader 的执行顺序是相反的:
run-loader 优先执行 PitchLoader,在执行 PitchLoader 时进行 loaderIndex++;
run-loader 之后执行 NormalLoader,在执行 NormalLoader 时进行 loaderIndex--;
修改loader执行顺序:
- rules:
[{test, use, loader, type, exclude, include, parser, generator, enforce},...]
,规则集合test:
reg
,匹配文件资源use:
[{loader, options, query},...]
,设置对匹配到的资源使用的loader及配置- loader:
string
,使用的loader。示例:use: [{loader: 'css-loader'}]
,简写:use: ['css-loader']
- options:
object
,loader的配置项。示例:use: [{loader: 'css-loader', options: {importLoaders: 1}}]
, - query:``,已被options替代
- 注意: use中多个loader的使用顺序是从后往前的
- loader:
loader:
,
Rule.use[{loader}]
的简写enforce:
pre | post | normal | inline
,用于控制 loader 执行顺序的选项。pre
:指定该 loader 在所有其他 loader 之前执行。常用于进行某些预处理,例如代码风格检查。post
:指定该 loader 在所有其他 loader 之后执行。常用于进行某些后处理,例如添加某些特性或优化。normal
:默认
,loader 将按照 Webpack 的默认顺序执行。inline
:在行内设置的 loader。如:import 'loader1!loader2!./test.js'
- js
rules: [ { test: /\.js$/, use: [{ loader: 'xxx-loader' }], enforce: 'pre' // 1 }, { // 2 test: /\.js$/, use: [{ loader: 'yyy-loader' }] }, { test: /\.js$/, use: [{ loader: 'zzz-loader' }], enforce: 'post' // 3 } ]
那么,能不能改变它们的执行顺序呢?
- 我们可以拆分成多个 Rule 对象,通过 enforce 来改变它们的顺序;
在 Pitching 和 Normal 它们的执行顺序分别是:
Pitching: post, inline, normal, pre;
Normal: pre, normal, inline, post;
同步、异步Loader
同步Loader
什么是同步的 Loader 呢?
默认创建的 Loader 就是同步的 Loader;
这个 Loader 必须通过 return 或者 this.callback 来返回结果,交给下一个 loader 来处理;
通常在有错误的情况下,我们会使用 this.callback;
this.callback 的用法如下:
- this.callback():
(err, content)
,回调函数- err:
Error | null
,错误信息。如果没错,则传入null - content:
string | buffer
,传递给下个loader的内容
- err:
异步Loader
什么是异步的 Loader 呢?
有时候我们使用 Loader 时会进行一些异步的操作;
我们希望在异步操作完成后,再返回这个 loader 处理的结果;
这个时候我们就要使用异步的 Loader 了;
loader-runner 已经在执行 loader 时给我们提供了方法,让 loader 变成一个异步的 loader:
// 通过调用this.async(),告诉loader不要在函数末尾直接return undefined,而是返回异步操作返回的结果
const callback = this.async()
参数
传入、获取参数
1、传递参数
2、获取参数
- 方式一:(废弃),早期需要使用 loader-utils 库来获取参数,目前已经不再需要
- 方式二:目前可以直接通过
this.getOptions()
方法获取参数
校验参数
1、我们可以通过一个 webpack 官方提供的校验库 schema-utils 安装对应的库:
npm install schema-utils -D
2、传递参数
3、校验规则
4、校验参数是否符合规则
5、校验失败
案例
mr-babel-loader
我们知道 babel-loader 可以帮助我们对 JavaScript 的代码进行转换,这里我们定义一个自己的 babel-loader:
一、依赖包: @babel/core
二、实现过程
1、使用 babel.transform()
方法转换js代码
此时打包会发现babel并没有转换ES6的语法为ES5
2、在使用babel-loader时,传递plugins、presets打包参数
3、在自定义的babel-loader中获取传递的options参数
4、添加参数校验
5、在babel.config.js
文件中配置babel参数
配置参数
获取参数
mr-md-loader
作用: hymd-loader用来解析markdown文件
依赖包:
- marked:marked 是一个基于JS的 Markdown 解析器和编译器。
- 安装:
pnpm i marked -D
- 安装:
- highlight.js:代码高亮插件。
- 安装:
pnpm i highlight.js -D
- 安装:
- marked-highlight:用于在Markdown中高亮代码的库。它将 marked 和 highlight.js结合。
- 安装:
pnpm i marked-highlight -D
- 安装:
实现过程:
1、使用mr-md-loader
解析md文件
2、mr-md-loader
基本实现
由于loader返回的结果必须是一个模块化的内容,此处在得到html文本后需要保存到code变量并导出出去。
3、在main.js
中导入md文件,并显示到页面中
4、显示效果
问题:此时的样式优点丑,需要优化样式
5、优化: 添加自定义的CSS样式
css样式
导入样式
配置css-loader
效果
6、优化: 高亮关键字
使用
highlight.js
插件标识出md内容的关键字自定义关键字的样式
使用
highlight.js
库默认的样式
自定义Plugin
Plugin API
tapable
tapable: 是一个用于处理插件系统的 JS 库,通常用于构建和扩展系统中的钩子(hooks)和事件。这是一个被广泛使用的库,尤其是在 Webpack 和其他构建工具中。
API:
实例方法
hook.tap():
(pluginName, fn)
,用于同步钩子的注册方法。它用于注册一个钩子函数,并指定一个插件名称。pluginName:
string
,插件名称。fn:
(...args) => void
,钩子函数。- js
hook.tap('SecondPlugin', (name, age) => { console.log('SecondPlugin:', name, age); });
hook.tapAsync():
(pluginName, fn)
,用于注册异步钩子的方法,适用于AsyncSeriesHook
和AsyncParallelHook
等异步钩子类型。pluginName:
string
,插件名称。fn:
(...args, callback) => void
,钩子函数。在操作完成后调用 callback()。- js
hookAsync.tapAsync('SecondPlugin', (name, callback) => { setTimeout(() => { console.log('SecondPlugin:', name); callback(); // 完成异步操作 }, 500); });
hook.call():
(...args)
,用于同步地触发所有注册的钩子函数。按注册顺序执行。...args:
string[]
,指定钩子所需的参数。- js
const { SyncHook } = require('tapable'); // 创建一个 SyncHook 实例 const hook = new SyncHook(['name', 'age']); // 注册钩子函数 hook.tap('PrintName', (name, age) => { console.log(`Name: ${name},Age: ${age}`); }); // 触发钩子 hook.call('Alice', 30);
hook.callAsync():
(...args, callback)
,用于触发异步钩子的一个方法,并在所有钩子完成后执行一个最终的回调。...args:
string[]
,传递给钩子函数的参数。callback:
(err?) => void
,所有钩子函数执行完成后的回调函数,接受一个可选的错误参数。- js
const { AsyncSeriesHook } = require('tapable'); // 创建 AsyncSeriesHook 实例 const hook = new AsyncSeriesHook(['arg1', 'arg2']); // 注册异步钩子 hook.tapAsync('Plugin1', (arg1, arg2, callback) => { setTimeout(() => { console.log('Plugin1:', arg1, arg2); callback(); // 异步操作完成后调用 callback }, 1000); }); hook.tapAsync('Plugin2', (arg1, arg2, callback) => { setTimeout(() => { console.log('Plugin2:', arg1, arg2); callback(); // 异步操作完成后调用 callback }, 500); }); // 触发钩子 hook.callAsync('value1', 'value2', (err) => { if (err) { console.error('Error:', err); } else { console.log('All hooks executed'); } });
创建实例
new SyncHook():
([arg1, arg2, ...])
,用于创建同步钩子的类。[arg1, arg2, ...]:
string[]
,定义了钩子函数所接受的参数列表。返回:
hook:
SyncHook
,一个 SyncHook 实例。- js
const { SyncHook } = require('tapable'); // 创建一个 SyncHook 实例 const hook = new SyncHook(['name', 'age']); // 注册钩子函数 hook.tap('PrintName', (name, age) => { console.log(`Name: ${name},Age: ${age}`); }); // 触发钩子 hook.call('Alice', 30);
new SyncBailHook():
([arg1, arg2, ...])
,用于创建同步钩子的类,与SyncHook
不同的是,它提供了一个“中断”机制。一旦一个钩子函数返回非undefined
的值,后续的钩子函数将不会被执行。[arg1, arg2, ...]:
string[]
,定义了钩子函数所接受的参数列表。返回:
hook:
SyncBailHook
,一个 SyncBailHook 实例。- js
const { SyncBailHook } = require('tapable'); // 创建一个 SyncBailHook 实例 const hook = new SyncBailHook(['data']); // 注册钩子函数 hook.tap('FirstPlugin', (data) => { console.log('FirstPlugin:', data); // 返回一个值,后续钩子将不会被调用 return 'Early exit'; }); hook.tap('SecondPlugin', (data) => { console.log('SecondPlugin:', data); }); // 触发钩子 hook.call('Hello, World!');
new SyncLoopHook():
([arg1, arg2, ...])
,用于创建同步钩子的类,允许注册的钩子函数在特定条件下重复执行。每个钩子函数在被调用时会持续执行直到它返回undefined
,然后停止循环。[arg1, arg2, ...]:
string[]
,定义了钩子函数所接受的参数列表。返回:
hook:
SyncLoopHook
,一个 SyncLoopHook 实例。- js
const { SyncLoopHook } = require('tapable'); // 创建一个 SyncLoopHook 实例 const hook = new SyncLoopHook(['count']); // 注册钩子函数 hook.tap('LoopPlugin', (count) => { console.log('Processing:', count); // 返回非 undefined 的值,继续循环 if (count > 0) { return count - 1; } // 返回 undefined,停止循环 return undefined; }); // 触发钩子 hook.call(5);
new SyncWaterfallHook():
([arg1, arg2, ...])
,用于创建同步钩子的类,它的特点是钩子函数的返回值会传递给下一个钩子函数。[arg1, arg2, ...]:
string[]
,定义了钩子函数所接受的参数列表。返回:
hook:
SyncWaterfallHook
,一个 SyncWaterfallHook 实例。- js
const { SyncWaterfallHook } = require('tapable'); // 创建一个 SyncWaterfallHook 实例 const hook = new SyncWaterfallHook(['data']); // 注册钩子函数 hook.tap('FirstPlugin', (data) => { console.log('FirstPlugin:', data); // 修改数据并传递给下一个钩子函数 return data + 1; }); hook.tap('SecondPlugin', (data) => { console.log('SecondPlugin:', data); // 修改数据并传递给下一个钩子函数 return data * 2; }); hook.tap('ThirdPlugin', (data) => { console.log('ThirdPlugin:', data); // 最后一个钩子函数的返回值将不会被传递给其他函数 return data - 3; }); // 触发钩子 hook.call(5);
new AsyncParallelHook():
([arg1, arg2, ...])
,用于创建异步并行执行的钩子。与同步钩子不同,异步并行钩子允许钩子函数并行执行而不是依次执行。[arg1, arg2, ...]:
string[]
,定义了钩子函数所接受的参数列表。返回:
hookAsync:
AsyncParallelHook
,一个 AsyncParallelHook 实例。- js
const { AsyncParallelHook } = require('tapable'); // 创建一个 AsyncParallelHook 实例 const hookAsync = new AsyncParallelHook(['name']); // 注册钩子函数 hookAsync.tapAsync('FirstPlugin', (name, callback) => { setTimeout(() => { console.log('FirstPlugin:', name); callback(); // 完成异步操作 }, 1000); }); hookAsync.tapAsync('SecondPlugin', (name, callback) => { setTimeout(() => { console.log('SecondPlugin:', name); callback(); // 完成异步操作 }, 500); }); // 触发钩子 hookAsync.callAsync('John', (err) => { if (err) { console.error('Error:', err); } else { console.log('All plugins have finished processing'); } });
new AsyncSeriesHook():
([arg1, arg2, ...])
,用于处理异步操作。会等待上一个异步的 Hook 执行完毕。[arg1, arg2, ...]:
string[]
,定义了钩子函数所接受的参数列表。返回:
hookAsync:
AsyncSeriesHook
,一个 AsyncSeriesHook 实例。- js
const { AsyncSeriesHook } = require('tapable'); // 创建 AsyncSeriesHook 实例 const hook = new AsyncSeriesHook(['arg1', 'arg2']); // 注册钩子 hook.tapAsync('MyPlugin1', (arg1, arg2, callback) => { // 异步操作 setTimeout(() => { console.log('Async operation complete'); callback(); // 需要调用 callback 来表示操作完成 }, 1000); }); // 会等MyPlugin1执行完毕才执行 hook.tapAsync('MyPlugin2', (arg1, arg2, callback) => { // 异步操作 setTimeout(() => { console.log('Async operation complete'); callback(); // 需要调用 callback 来表示操作完成 }, 1000); }); // 触发钩子 hook.callAsync('value1', 'value2', (err) => { if (err) { console.error('Error:', err); } else { console.log('All hooks executed'); } });
node-ssh
node-ssh:是一个用于在 Node.js 中简化 SSH 连接和命令执行的库。它提供了一种方便的方式来进行远程管理和自动化操作。
API:
new NodeSSH():
()
,创建SSH实例。返回
ssh:
NodeSSH
,SSH实例- js
const { NodeSSH } = require('node-ssh'); const ssh = new NodeSSH();
ssh.connect():
({host, port, username, password, privateKey})
,连接到远程服务器。host:
string
,远程服务器的主机名或 IP 地址。port:
number
,默认:22
,SSH 端口。username:
string
,登录用户名。password:
string
,登录密码(如果你使用密码认证)。privateKey:
string
,私钥文件路径(如果你使用密钥认证)。返回: Promise
promise:
() => void
,- js
await ssh.connect({ host: 'example.com', username: 'your-username', password: 'your-password' });
ssh.execCommand():
(command,options?)
,在远程主机上执行linux命令。command:
string
,要执行的linux命令。options?:
{cwd, stdin}
,命令执行的选项,包括cwd
(当前工作目录) 和stdin
(标准输入数据)。返回: Promise
result:
({stdout, stderr}) => void
,返回一个包含stdout
和stderr
的Promise对象。- js
const result = await ssh.execCommand('ls -la'); console.log('STDOUT:', result.stdout); console.log('STDERR:', result.stderr);
ssh.putFile():
(localPath, remotePath)
,将本地文件上传到远程主机。localPath:
string
,本地文件的路径。remotePath:
string
,远程主机上的目标路径。返回: Promise
- js
await ssh.putFile('local/file/path', 'remote/file/path');
ssh.getFile():
(localPath, remotePath)
,从远程主机下载文件到本地。localPath:
string
,本地目标路径。remotePath:
string
,远程文件的路径。返回: Promise
- js
await ssh.getFile('local/file/path', 'remote/file/path');
ssh.putDirectory():
(localDir, remoteDir,options?)
,将本地目录递归地上传到远程服务器。localDir:
string
,本地目录的路径,指定要上传的目录。remoteDir:
string
,远程服务器上的目标目录路径。options?:
{recursive?, concurrency?, overwrite?, tick?}
,配置上传的选项。- recursive?:
boolean
,默认:true
,是否递归地上传子目录 - concurrency?:
number
,默认:10
,并发上传的并发数。 - overwrite?:
boolean
,默认:true
,是否覆盖远程目录中已存在的文件。 - tick?:
(localPath, remotePath, error) => void
,回调函数,接收每个文件的上传进度。
- recursive?:
返回: Promise
- js
const localDir = path.resolve(__dirname, 'local-directory'); const remoteDir = '/path/on/remote/server'; await ssh.putDirectory(localDir, remoteDir, { recursive: true, overwrite: true, tick: (localPath, remotePath, error) => { if (error) { console.error(`Failed to upload ${localPath}:`, error); } else { console.log(`Uploaded ${localPath} to ${remotePath}`); } } });
ssh.getDirectory():
(localDir, remoteDir,options?)
,从远程服务器递归地下载目录到本地。localDir:
string
,本地目标目录的路径。remoteDir:
string
,远程服务器上的源目录路径。options?:
{recursive?, concurrency?, overwrite?, tick?}
,配置下载的选项。- recursive?:
boolean
,默认:true
,是否递归地下载子目录 - concurrency?:
number
,默认:10
,并发下载的并发数。 - overwrite?:
boolean
,默认:true
,是否覆盖远程目录中已存在的文件。 - tick?:
(localPath, remotePath, error) => void
,回调函数,接收每个文件的下载进度。
- recursive?:
- js
const remoteDir = '/path/on/remote/server'; const localDir = path.resolve(__dirname, 'local-directory'); await ssh.getDirectory(remoteDir, localDir, { recursive: true, overwrite: true, tick: (remotePath, localPath, error) => { if (error) { console.error(`Failed to download ${remotePath}:`, error); } else { console.log(`Downloaded ${remotePath} to ${localPath}`); } } });
ssh.dispose():
()
,关闭与远程主机的连接并清理资源。- js
ssh.dispose();
Tapable
Tapable概述
我们知道 webpack 有两个非常重要的类:Compiler 和 Compilation
他们通过注入插件的方式,来监听 webpack 的所有生命周期;
插件的注入离不开各种各样的 Hook,而他们的 Hook 是如何得到的呢?
其实是创建了 Tapable 库中的各种 Hook 的实例;
所以,如果我们想要学习自定义插件,最好先了解一个库:Tapable
Tapable 是官方编写和维护的一个库;
Tapable 管理着需要的Hook,这些Hook可以被应用到我们的插件中;
Tapable的Hook
同步和异步的:
以 sync 开头的,是同步的Hook。
以 async 开头的,是异步的Hook,两个事件处理回调,不会等待上一次处理回调结束后再执行下一次回调。
其他的类别:
Bail:当有返回值时,就不会执行后续的事件触发了;
Loop:当返回值为 true,就会反复执行该事件,当返回值为 undefined 或者不返回内容,就退出事件,执行下一个事件;
Waterfall:当返回值不为 undefined 时,会将这次返回的结果作为下次事件的第一个参数;
Parallel:并行,不会等到上一个事件回调执行结束,才执行下一次事件处理回调;
Series:串行,会等待上一个异步的 Hook;
Hook的使用
依赖包:
- tapable:通过提供 Hooks 系统,使得你可以在 Webpack 的构建流程中插入自定义的逻辑。
- 安装:
pnpm i tapable
- 安装:
sync-基本使用
1、创建Hook对象
2、监听Hook中的事件
注意: 自定义的插件就是写在这个位置
3、触发事件
sync-bail使用
Bail:当有返回值时,就不会执行后续的事件触发了。
1、创建bailHook
2、监听Hook中的事件
3、触发事件
sync-loop使用
Loop:当返回值为 true,就会反复执行该事件,当返回值为 undefined 或者不返回内容,就退出事件,执行下一个事件;
1、创建loopHook
2、监听Hook中的事件
3、触发事件
sync-waterfall使用
Waterfall:当返回值不为 undefined 时,会将这次返回的结果作为下次事件的第一个参数;
1、创建waterfallHook
2、监听Hook中的事件
3、触发事件
async-parallel使用
Parallel:并行,不会等到上一个事件回调执行结束,才执行下一次事件处理回调;
1、创建parallelHook
2、监听Hook中的事件
3、触发事件
async-series使用
Series:串行,会等待上一个异步的 Hook;
1、创建seriesHook
2、监听Hook中的事件
3、触发事件
自定义Plugin
在之前的学习中,我们已经使用了非常多的 Plugin:
CleanWebpackPlugin
HTMLWebpackPlugin
MiniCSSExtractPlugin
CompressionPlugin
等等。。。
这些 Plugin 是如何被注册到 webpack 的生命周期中的呢?
第一:在 webpack 函数的 createCompiler 方法中,注册了所有的插件;
第二:在注册插件时,会调用插件函数或者插件对象的 apply 方法;
第三:插件方法会接收 compiler 对象,我们可以通过 compiler 对象来监听 Hook 的事件;
第四:某些插件也会传入一个 compilation 的对象,我们也可以监听 compilation 的 Hook 事件;
auto-upload-webpack-plugin
如何开发自己的插件呢?
目前大部分插件都可以在社区中找到,但是推荐尽量使用在维护,并且经过社区验证的;
这里我们开发一个自己的插件:将静态文件自动上传服务器中;
依赖包:
- node-ssh:在node中通过ssh连接远程服务器
- 安装:
pnpm i node-ssh -D
- 安装:
自定义插件:
1、创建 AutoUploadWebpackPlugin 类;
2、编写 apply 方法:
获取输出文件夹路径
通过 ssh 连接服务器;
删除服务器原来的文件夹;
上传文件夹中的内容;
const { NodeSSH } = require('node-ssh')
const { PASSWORD } = require('./config')
class AutoUploadWebpackPlugin {
constructor(options) {
this.ssh = new NodeSSH()
this.options = options
}
apply(compiler) {
// console.log("AutoUploadWebpackPlugin被注册:")
// 完成的事情: 注册hooks监听事件
// 等到assets已经输出到output目录上时, 完成自动上传的功能
compiler.hooks.afterEmit.tapAsync("AutoPlugin", async (compilation, callback) => {
// 1.获取输出文件夹路径(其中资源)
const outputPath = compilation.outputOptions.path
// 2.连接远程服务器 SSH
await this.connectServer()
// 3.删除原有的文件夹中内容
const remotePath = this.options.remotePath
this.ssh.execCommand(`rm -rf ${remotePath}/*`)
// 4.将文件夹中资源上传到服务器中
await this.uploadFiles(outputPath, remotePath)
// 5.关闭ssh连接
this.ssh.dispose()
// 完成所有的操作后, 调用callback()
callback()
})
}
async connectServer() {
await this.ssh.connect({
host: this.options.host,
username: this.options.username,
password: this.options.password
})
console.log('服务器连接成功')
}
async uploadFiles(localPath, remotePath) {
const status = await this.ssh.putDirectory(localPath, remotePath, {
recursive: true,
concurrency: 10
})
if (status) {
console.log("文件上传服务器成功~")
}
}
}
module.exports = AutoUploadWebpackPlugin
module.exports.AutoUploadWebpackPlugin = AutoUploadWebpackPlugin
3、在 webpack 的 plugins 中,使用 AutoUploadWebpackPlugin 类;
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const AutoUploadWebpackPlugin = require('./plugins/AutoUploadWebpackPlugin')
const { PASSWORD } = require('./plugins/config')
module.exports = {
entry: "./src/main.js",
output: {
path: path.resolve(__dirname, "./build"),
filename: "bundle.js"
},
plugins: [
new HtmlWebpackPlugin(),
new AutoUploadWebpackPlugin({
host: "123.207.32.32",
username: "root",
password: PASSWORD,
remotePath: "/root/test"
})
]
}